/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgement:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgement may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or NaradaBrokering, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called NaradaBrokering, 
 * nor may Indiana University or Community Grids Lab or NaradaBrokering appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 *LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 *WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 *NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 *INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 *INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 *"VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 *LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 *ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 *GENERATED USING SOFTWARE.
 */
package cgl.narada.util.webserver;

/*
 * Modified web server example on SUN's java website.  Used as a simple
 * web server for display performance data, setting config parameters, etc.
 *
 */
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.Date;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import cgl.narada.util.Log;

public class WebServer extends Thread {

    public static Hashtable s_pageMap = new Hashtable();
    protected boolean  s_fileEnabled = false;

    protected Properties props = new Properties(); // Server config
    Vector threads = new Vector();  // Idle worker threads
    File root;        // web server's virtula root
    int timeout = 0;  // Timeout on client connections.
    int workers = 1;  // Max # of worker threads.
    int port    = 8080;
    private boolean m_stopped = false;
    private ServerSocket m_serverSocket = null;

    /**
     * -----------------------------------------------------------------
     */
    public void setMapping(String file,String page) {
        s_pageMap.put(file,page);
    }

    /**
     * -----------------------------------------------------------------
     */
    public void setMapping(String file,WebCGI cgi) {
        s_pageMap.put(file,cgi);
    }

    /**
     * -----------------------------------------------------------------
     * Set whether file access is enabled.  By default is false.
     */
    public void setFileEnabled(boolean enabled) {
        s_fileEnabled = enabled;
    }

    /**
     * -----------------------------------------------------------------
     */
    public void destroy() {
        if (!m_stopped) {
            m_stopped = true;
            interrupt();
        }
    }

    /**
     * -----------------------------------------------------------------
     */
    public void run() {
        /* start worker threads */
        for (int i = 0; i < workers; ++i) {
            Worker w = new Worker();
            (new Thread(w, "worker #"+i)).start();
            threads.addElement(w);
        }

        while (!m_stopped) {
            Socket s = null;
            try {
                s = m_serverSocket.accept();
            } catch (IOException e) {
                continue;
            }

            Worker w = null;
            synchronized (threads) {
                if (threads.isEmpty()) {
                    Worker ws = new Worker();
                    ws.setSocket(s);
                    (new Thread(ws, "additional worker")).start();
                } else {
                    w = (Worker) threads.elementAt(0);
                    threads.removeElementAt(0);
                    w.setSocket(s);
                }
            }
        }
    }

    /**
     * -----------------------------------------------------------------
     * Sets the web server port.  Default is 8080.  needs to be called before
     * goServer.
     */
    public void setPort(int p) {
        port = p;
    }

    /**
     * -----------------------------------------------------------------
     */
    public void goServer() throws Exception {
        loadProps();
        printProps();
        m_serverSocket = new ServerSocket(port);
        start();
    }


    /**
     * -----------------------------------------------------------------
     */
    protected static void p(String s) {
        Log.log("WebServer",s);
    }

    /**
     * -----------------------------------------------------------------
     */
    protected static void log(String s) {
        p(s);
    }

    /**
     * -----------------------------------------------------------------
     */
    private void loadProps() throws IOException {
        File f = new File
                (System.getProperty("java.home")+File.separator+
                    "lib"+File.separator+"www-server.properties");
        if (f.exists()) {
            InputStream is =new BufferedInputStream(new
                           FileInputStream(f));
            props.load(is);
            is.close();
            String r = props.getProperty("root");
            if (r != null) {
                root = new File(r);
                if (!root.exists()) {
                    throw new Error(root + " doesn't exist as server root");
                }
            }
            r = props.getProperty("timeout");
            if (r != null) {
                timeout = Integer.parseInt(r);
            }
            r = props.getProperty("workers");
            if (r != null) {
                workers = Integer.parseInt(r);
            }
        }

        /* if no properties were specified, choose defaults */
        if (root == null) {
            root = new File(System.getProperty("user.dir"));
        }
        if (timeout <= 1000) {
            timeout = 5000;
        }
        if (workers < 1) {
            workers = 1;
        }
    }

    /**
     * -----------------------------------------------------------------
     */
    protected void printProps() {
        //p("root="+root);
        p("timeout="+timeout);
        p("workers="+workers);
    }

    /**
     * ========================================================================
     * Worker Thread.
     *
     * ========================================================================
     */

    class Worker extends WebServer implements HttpConstants, Runnable {
        final int BUF_SIZE = 4096;

        final byte[] EOL = {(byte)'\r', (byte)'\n' };

        /* buffer to use for requests */
        byte[] buf;
        /* Socket to client we're handling */
        private Socket s;

        Worker() {
            buf = new byte[BUF_SIZE];
            s = null;
        }

        synchronized void setSocket(Socket s) {
            this.s = s;
            notify();
        }

        public synchronized void run() {
            while(true) {
                if (s == null) {
                    /* nothing to do */
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        /* should not happen */
                        continue;
                    }
                }
                try {
                    handleClient();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                /* go back in wait queue if there's fewer
                 * than numHandler connections.
                 */
                s = null;
                Vector pool = threads;
                synchronized (pool) {
                    if (pool.size() >= workers) {
                        /* too many threads, exit this one */
                        return;
                    } else {
                        pool.addElement(this);
                    }
                }
            }
        }

        void handleClient() throws IOException {
            InputStream is = new BufferedInputStream(s.getInputStream());
            PrintStream ps = new PrintStream(s.getOutputStream());
            /* we will only block in read for this many milliseconds
             * before we fail with java.io.InterruptedIOException,
             * at which point we will abandon the connection.
             */
            s.setSoTimeout(timeout);
            s.setTcpNoDelay(true);
            /* zero out the buffer from last time */
            for (int i = 0; i < BUF_SIZE; i++) {
                buf[i] = 0;
            }
            try {
                /* We only support HTTP GET/HEAD, and don't
                 * support any fancy HTTP options,
                 * so we're only interested really in
                 * the first line.
                 */
                int nread = 0, r = 0;

    outerloop:
                while (nread < BUF_SIZE) {
                    r = is.read(buf, nread, BUF_SIZE - nread);
                    if (r == -1) {
                        /* EOF */
                        return;
                    }
                    int i = nread;
                    nread += r;
                    int numEnter = 0;
                    for (; i < nread; i++) {
                        if (buf[i] == (byte)'\n' || buf[i] == (byte)'\r') {
                            numEnter++;
                            if (numEnter >= 2) {
                                /* read one line */
                                break outerloop;
                            }
                        } else {
                            numEnter = 0;
                        }
                    }
                }

                /* are we doing a GET or just a HEAD */
                boolean doingGet;
                /* beginning of file name */
                int index;
                if (buf[0] == (byte)'G' &&
                    buf[1] == (byte)'E' &&
                    buf[2] == (byte)'T' &&
                    buf[3] == (byte)' ') {
                    doingGet = true;
                    index = 4;
                } else if (buf[0] == (byte)'H' &&
                           buf[1] == (byte)'E' &&
                           buf[2] == (byte)'A' &&
                           buf[3] == (byte)'D' &&
                           buf[4] == (byte)' ') {
                    doingGet = false;
                    index = 5;
                } else {
                    /* we don't support this method */
                    ps.print("HTTP/1.0 " + HTTP_BAD_METHOD +
                               " unsupported method type: ");
                    ps.write(buf, 0, 5);
                    ps.write(EOL);
                    ps.flush();
                    s.close();
                    return;
                }

                int i = 0;
                /* find the file name, from:
                 * GET /foo/bar.html HTTP/1.0
                 * extract "/foo/bar.html"
                 */
                for (i = index; i < nread; i++) {
                    if (buf[i] == (byte)' ') {
                        break;
                    }
                }

                String fname = new String(buf,index,i-index);
                if (fname.startsWith("/")) {
                    fname = fname.substring(1);
                }
                int qIndex = fname.indexOf("?");
                String params = "";

                if (fname.indexOf("?") >= 0) {
                    params = fname.substring(qIndex+1,fname.length());
                    fname = fname.substring(0,qIndex);
                }

                Object page = WebServer.s_pageMap.get(fname);
                if (page == null) {
                    fname = fname.replace('/', File.separatorChar);
                    File targ = new File(root, fname);
                    if (targ.isDirectory()) {
                        File ind = new File(targ, "index.html");
                        if (ind.exists()) {
                            targ = ind;
                        }
                    }
                    boolean OK = printHeaders(targ, ps);
                    if (doingGet) {
                        if (OK && s_fileEnabled) {
                            sendFile(targ, ps);
                        } else {
                            send404(targ, ps);
                        }
                    }
                } else {
                    String result = "";
                    if (page instanceof WebCGI) {
                        result = ((WebCGI)page).execute(extractParameters(params));
                    } else {
                        result = (String)page;
                    }
                    boolean OK = printHeaders(result, ps);
                    if (OK) {
                        ps.write(result.getBytes());
                    } else {
                        send404(null,ps);
                    }
                }
            } finally {
                s.close();
            }
        }

        Properties extractParameters(String params) {
            Properties prop = new Properties();

            StringTokenizer tokenizer = new StringTokenizer(params,"&");
            while(tokenizer.hasMoreElements()) {
                String token = tokenizer.nextToken();
                int eqIndex = token.indexOf("=");
                if (eqIndex < 0) {
                    prop.put(token,"");
                } else {
                    String key = token.substring(0,eqIndex);
                    String val = token.substring(eqIndex+1,token.length());
                    prop.put(URLDecoder.decode(key),URLDecoder.decode(val));
                }
            }
            return prop;
        }

        boolean printHeaders(String page, PrintStream ps) throws IOException {
            boolean ret = false;
            int rCode = HTTP_OK;
            ps.print("HTTP/1.0 " + HTTP_OK+" OK");
            ps.write(EOL);
            ret = true;
            ps.print("Server: Simple java");
            ps.write(EOL);
            ps.print("Date: " + (new Date()));
            ps.write(EOL);
            ps.print("Content-length: "+page.length());
            ps.write(EOL);
            ps.print("Last Modified: " + (new  Date(System.currentTimeMillis())));
            ps.write(EOL);
            ps.print("Content-type: text/html");
            ps.write(EOL);
            ps.write(EOL);
            return ret;
        }

        boolean printHeaders(File targ, PrintStream ps) throws IOException {
            boolean ret = false;
            int rCode = 0;
            if (!targ.exists()) {
                rCode = HTTP_NOT_FOUND;
                ps.print("HTTP/1.0 " + HTTP_NOT_FOUND + " not found");
                ps.write(EOL);
                ret = false;
            }  else {
                rCode = HTTP_OK;
                ps.print("HTTP/1.0 " + HTTP_OK+" OK");
                ps.write(EOL);
                ret = true;
            }
            log("From " +s.getInetAddress().getHostAddress()+": GET " +
                targ.getAbsolutePath()+"-->"+rCode);
            ps.print("Server: Simple java");
            ps.write(EOL);
            ps.print("Date: " + (new Date()));
            ps.write(EOL);
            if (ret) {
                if (!targ.isDirectory()) {
                    ps.print("Content-length: "+targ.length());
                    ps.write(EOL);
                    ps.print("Last Modified: " + (new
                                  Date(targ.lastModified())));
                    ps.write(EOL);
                    String name = targ.getName();
                    int ind = name.lastIndexOf('.');
                    String ct = null;
                    if (ind > 0) {
                        ct = (String) map.get(name.substring(ind));
                    }
                    if (ct == null) {
                        ct = "unknown/unknown";
                    }
                    ps.print("Content-type: " + ct);
                    ps.write(EOL);
                } else {
                    ps.print("Content-type: text/html");
                    ps.write(EOL);
                }
            }
            return ret;
        }

        void send404(File targ, PrintStream ps) throws IOException {
            ps.write(EOL);
            ps.write(EOL);
            ps.println("Not Found\n\n"+
                       "The requested resource was not found.\n");
        }

        void sendFile(File targ, PrintStream ps) throws IOException {
            InputStream is = null;
            ps.write(EOL);
            if (targ.isDirectory()) {
                listDirectory(targ, ps);
                return;
            } else {
                is = new FileInputStream(targ.getAbsolutePath());
            }

            try {
                int n;
                while ((n = is.read(buf)) > 0) {
                    ps.write(buf, 0, n);
                }
            } finally {
                is.close();
            }
        }

        void listDirectory(File dir, PrintStream ps) throws IOException {
            ps.println("<TITLE>Directory listing</TITLE><P>\n");
            ps.println("<A HREF=\"..\">Parent Directory</A><BR>\n");
            String[] list = dir.list();
            for (int i = 0; list != null && i < list.length; i++) {
                File f = new File(dir, list[i]);
                if (f.isDirectory()) {
                    ps.println("<A HREF=\""+list[i]+"/\">"+list[i]+"/</A><BR>");
                } else {
                    ps.println("<A HREF=\""+list[i]+"\">"+list[i]+"</A><BR");
                }
            }
            ps.println("<P><HR><BR><I>" + (new Date()) + "</I>");
        }

    }

    interface HttpConstants {
        /** 2XX: generally "OK" */
        public static final int HTTP_OK = 200;
        public static final int HTTP_CREATED = 201;
        public static final int HTTP_ACCEPTED = 202;
        public static final int HTTP_NOT_AUTHORITATIVE = 203;
        public static final int HTTP_NO_CONTENT = 204;
        public static final int HTTP_RESET = 205;
        public static final int HTTP_PARTIAL = 206;

        /** 3XX: relocation/redirect */
        public static final int HTTP_MULT_CHOICE = 300;
        public static final int HTTP_MOVED_PERM = 301;
        public static final int HTTP_MOVED_TEMP = 302;
        public static final int HTTP_SEE_OTHER = 303;
        public static final int HTTP_NOT_MODIFIED = 304;
        public static final int HTTP_USE_PROXY = 305;

        /** 4XX: client error */
        public static final int HTTP_BAD_REQUEST = 400;
        public static final int HTTP_UNAUTHORIZED = 401;
        public static final int HTTP_PAYMENT_REQUIRED = 402;
        public static final int HTTP_FORBIDDEN = 403;
        public static final int HTTP_NOT_FOUND = 404;
        public static final int HTTP_BAD_METHOD = 405;
        public static final int HTTP_NOT_ACCEPTABLE = 406;
        public static final int HTTP_PROXY_AUTH = 407;
        public static final int HTTP_CLIENT_TIMEOUT = 408;
        public static final int HTTP_CONFLICT = 409;
        public static final int HTTP_GONE = 410;
        public static final int HTTP_LENGTH_REQUIRED = 411;
        public static final int HTTP_PRECON_FAILED = 412;
        public static final int HTTP_ENTITY_TOO_LARGE = 413;
        public static final int HTTP_REQ_TOO_LONG = 414;
        public static final int HTTP_UNSUPPORTED_TYPE = 415;

        /** 5XX: server error */
        public static final int HTTP_SERVER_ERROR = 500;
        public static final int HTTP_INTERNAL_ERROR = 501;
        public static final int HTTP_BAD_GATEWAY = 502;
        public static final int HTTP_UNAVAILABLE = 503;
        public static final int HTTP_GATEWAY_TIMEOUT = 504;
        public static final int HTTP_VERSION = 505;
    }

    /* mapping of file extensions to content-types */
    static java.util.Hashtable map = new java.util.Hashtable();

    static {
        fillMap();
    }

    static void setSuffix(String k, String v) {
        map.put(k, v);
    }

    static void fillMap() {
        setSuffix("", "content/unknown");
        setSuffix(".uu", "application/octet-stream");
        setSuffix(".exe", "application/octet-stream");
        setSuffix(".ps", "application/postscript");
        setSuffix(".zip", "application/zip");
        setSuffix(".sh", "application/x-shar");
        setSuffix(".tar", "application/x-tar");
        setSuffix(".snd", "audio/basic");
        setSuffix(".au", "audio/basic");
        setSuffix(".wav", "audio/x-wav");
        setSuffix(".gif", "image/gif");
        setSuffix(".jpg", "image/jpeg");
        setSuffix(".jpeg", "image/jpeg");
        setSuffix(".htm", "text/html");
        setSuffix(".html", "text/html");
        setSuffix(".text", "text/plain");
        setSuffix(".c", "text/plain");
        setSuffix(".cc", "text/plain");
        setSuffix(".c++", "text/plain");
        setSuffix(".h", "text/plain");
        setSuffix(".pl", "text/plain");
        setSuffix(".txt", "text/plain");
        setSuffix(".java", "text/plain");
    }


}


